# Класс и экземпляр класса

`Python` имеет множество встроенных типов, например, `int, str` и так далее, которые мы можем использовать в программе. 
Но также Python позволяет определять `собственные типы` с помощью `классов`.
Класс представляет некоторую сущность. Конкретным воплощением класса является объект.

## Трансформация от функционального мышления к ОО-мышлению

Вокруг нас все объекты – человек, машина, здания, дома, компьютеры.

Можно провести следующую аналогию. У нас у всех есть некоторое представление о человеке, у которого есть имя, возраст, какие-то другие характеристики. 

Человек может выполнять некоторые действия - ходить, бегать, думать и т.д. 

То есть это представление, которое включает набор характеристик и действий, можно назвать классом. 

Конкретное воплощение этого шаблона может отличаться, например, одни люди имеют одно имя, другие - другое имя. 

И реально существующий человек будет представлять объект этого класса.

<details>
<summary>...</summary>


И объекты между собой общаются, взаимодействуют. 
В программировании общаются между собой только передачей сообщений.

В общем базовая концепция ООП состоит из двух пунктов – всё есть объект и объекты общаются между собой путем передачи сообщений.

Причем `Python` не 100 % ООП-ориентированный язык. 
Если есть объект, то его нужно как-то классифицировать, как-то описать.

Обычно это сравнивают с чертежом, чтобы построить дом надо сделать чертеж.

Или если вспомнить зоологию, корова – копытные есть — значит класс парнокопытные, класс млекопитающих парнокопытные.

</details>

И в ООП такие же классы

```python
class ИмяКласса:
    pass

type(ИмяКласса)
```

В данном случае в классе не определяется никаких методов или атрибутов.

Однако поскольку в нем должно быть что-то определено, то в качестве заменителя функционала класса применяется оператор pass. 

Этот оператор применяется, когда синтаксически необходимо определить некоторый код, однако исходя из задачи код нам не нужен, и вместо конкретного кода вставляем оператор `pass`.

Итак, мы видим результат `<class 'type'>`. 

! На самом деле класс – это объект для описания других объектов

В блоке `ИмяКласса` мы и будем описывать атрибутику, поведение, методы. 

Вообще всё, что он из себя представляет. 

Пока он пустой, но мы уже можем создавать объекты этого класс. 

Конвеницально принято называть имена классов с большой буквы.

Обращение также как как функции, но не забываем про большую букву


``` python
объект = ИмяКласса()
print (type(объект))
```

Запустим 

> `<class '__main__.ИмяКласса'>`

Как проверить, является ли наш объект экземплярам конкретного класса

``` python
isinstance(объект, ИмяКласса)
```
это функция – предикат, возвращает булев тип. 

Ну и соответственно возвращает 

``` python
print (isinstance(объект, list))
```

### Пример

Предположим, мы хотим определить класс для хранения координат точек на плоскости. 

Для начала я его запишу без какого-либо содержимого, только имя класса `Point` и все:

```python
class Point:
    pass
```

Здесь оператор `pass` указывает, что мы в классе ничего не определяем. 

Также обратите внимание, что  в соответствии со стандартом `PEP8` имя класса принято записывать с заглавной буквы. 

И, конечно же, называть так, чтобы оно отражало суть этого класса. 

В дальнейшем мы будем придерживаться этого правила.

Итак, у нас получилось простейшее определение класса с именем `Point`. Но в таком виде он не особо полезен. 

Пропишем в нем два атрибута: `color` – цвет точек; `circle` – радиус точек:

```python
class Point:
    color = 'red'
    circle = 2
```

Обратите внимание, переменные внутри класса обычно называются атрибутами класса или его свойствами. 

Теперь в нашем классе есть два атрибута `color` и `circle`. 

Но, как правильно воспринимать эту конструкцию? 

Фактически, сам класс образует пространство имен, в данном случае с именем `Point`, в котором находятся две переменные `color` и `circle`. 

И мы можем обращаться к ним, используя синтаксис для пространства имен, например:

```python
Point.color = 'black'
```

или для считывания значения:

```python
Point.circle
```
(В консольном режиме увидим значение 2). А чтобы увидеть все атрибуты класса можно обратиться к специальной коллекции `__dict__`:

```python
Point.__dict__
```

Здесь отображается множество служебных встроенных атрибутов и среди них есть два наших: `color и circle`.

Теперь сделаем следующий шаг и создадим экземпляры этого класса. 

В нашем случае для создания объекта класса Point достаточно после его имени прописать круглые скобки:

```python
a = Point()
```

Смотрите, справа на панели в `Python Console` у нас появилась переменная `a`, через которую доступны два атрибута класса: `color и circle`.

Давайте создадим еще один объект этого класса:

```python
b = Point()
```
Появилась переменная `b`, которая ссылается на новый объект (он расположен по другому адресу) и в этом объекте мы также видим два атрибута класса `Point`.

По аналогии можно создавать произвольное количество экземпляров класса.

С помощью функции `type` мы можем посмотреть тип данных для переменных `a` или `b`:

```python
type(a)
```
Видим, что это класс `Point`. Эту принадлежность можно проверить, например, так:

```python
type(a) == Point
```
или так:

```python
isinstance(a, Point)
```

![](img/simple-class.png)

Во-первых, объекты `a и b` образуют свое пространство имен – пространство имен экземпляров класса и, во-вторых, не содержат никаких собственных атрибутов. 

Свойства `color и circle` принадлежат непосредственно классу `Point` и находятся в нем, а объекты `a и b` лишь имеют ссылки на эти атрибуты класса. 

Поэтому их не случайно называют именно атрибутами класса, подчеркивая этот факт. 

То есть, атрибуты класса – общие для всех его экземпляров. 

И мы можем легко в этом убедиться.

Давайте изменим значение свойства `circle на 1`:
```python
Point.circle = 1
```
И в обоих объектах это свойство стало равно 1. Мало того, если посмотреть коллекцию `__dict__ `у объектов:

```python
a.__dict__
```
то она будет пустой, так как в наших экземплярах отсутствуют какие-либо атрибуты. Но, тем не менее, мы можем через них обращаться к атрибутам класса:

```python
a.color
b.circle
```
Но, если мы выполним присваивание, например:

```python
a.color = 'green'
```
То, смотрите, в объекте a свойство color стало 'green', а в b – прежнее. 

Почему? 

Дело в том, что мы здесь через переменную a обращаемся к пространству имен уже экземпляра класса и оператор присваивания в Python создает новую переменную, если она отсутствует в текущей локальной области видимости, то есть, создается атрибут `color` уже непосредственно в объекте `a`:


![](img/__dict__.png)


То есть, мы с вами создали локальное свойство в объекте `a`. 

Этот момент нужно очень хорошо знать и понимать. 

На этом принципе в Python построено формирование атрибутов классов и локальных атрибутов их экземпляров.

Добавление и удаление атрибутов класса
Кстати, по аналогии, мы можем создавать новые атрибуты и в классе, например, так:

```
Point.type_pt = 'disc'
```
Или то же самое можно сделать с помощью специальной функции:

```python
setattr(Point, 'prop', 1)
```
Она создает новый атрибут в указанном пространстве имен (в данном случае в классе `Point`) с заданным значением. 

Если эту функцию применить к уже существующему атрибуту:

```python
setattr(Point, 'type_pt', 'square')
```
то оно будет изменено на новое значение.

Если же мы хотим прочитать какое-либо значение атрибута, то достаточно обратиться к нему. 

В консольном режиме это выглядит так:

```python
Point.circle
```
Но, при обращении к несуществующему атрибуту класса, например:

```python
Point.a
```
возникнет ошибка. Этого можно избежать, если воспользоваться специальной встроенной функцией:

```python
getattr(Point, 'a', False)
```
Здесь третий аргумент – возвращаемое значение, если атрибут не будет найден.

Эту же функцию можно вызвать и с двумя аргументами:

```python
getattr(Point, 'a')
```
Но тогда также будет сгенерирована ошибка при отсутствии указанного атрибута. Иначе:

```python
getattr(Point, 'color')
```
она возвратит его значение. 

То есть, эта функция дает нам больше гибкости при обращении к атрибутам класса. 
Хотя на практике ей пользуются только в том случае, если есть опасность обращения к несуществующим атрибутам. 
Обычно, все же, применяют обычный синтаксис:

```python
Point.color
```
Наконец, мы можем удалять любые атрибуты из класса. 

Сделать это можно, по крайней мере, двумя способами.

Первый – это воспользоваться оператором `del`:

```python
del Point.prop
```
Если повторить эту команду и попытаться удалить несуществующий атрибут, возникнет ошибка. 

Поэтому перед удалением рекомендуется проверять существование удаляемого свойства. 
Делается это с помощью функции `hasattr`:

```python
hasattr(Point, 'prop')
```
Она возвращает `True`, если атрибут найден и `False` – в противном случае.

Также удалить атрибут можно с помощью функции:

```python
delattr(Point, 'type_pt')
```
Она работает аналогично оператору `del`.

И, обратите внимание, удаление атрибутов выполняется только в текущем пространстве имен. Например, если попытаться удалить свойство `color` из объекта `b`:

```python
del b.color
```

то получим ошибку, т.к. в объекте `b` не своих локальных свойств и удалять здесь в общем то нечего. 

А вот в объекте a есть свое свойство `color`, которое мы с вами добавляли:

```python
a.__dict__
```

и его можно удалить:
```python
del a.color
```

Смотрите, после удаления локального свойства `color` в объекте a становится доступным атрибут `color` класса `Point` с другим значение `black`.

И это логично, т.к. если свойство не обнаруживается в локальной области, то поиск продолжается в следующей (внешней) области видимости. 

А это (для объекта a) класс `Point`. 

Вот этот момент также следует хорошо понимать при работе с локальными свойствами объектов и атрибутами класса.

### Атрибуты экземпляров классов

Теперь, когда мы знаем, как создаются атрибуты, вернемся к нашей задаче формирования объектов точек на плоскости. 

Мы полагаем, что атрибуты `color` и `circle` класса `Point` – это общие данные для всех объектов этого класса. 

А вот координаты точек должны принадлежать его экземплярам. 

Поэтому для объектов `a` и `b` мы определим локальные свойства `x` и `y`:
```python
a.x = 1
a.y = 2
b.x = 10
b.y = 20
```

То есть, свойства `x`, `y` будут существовать непосредственно в объектах, но не в самом классе `Point`:

![](img/class-attributs.png)


В результате, каждый объект представляет точку с независимыми координатами на плоскости. 

А цвет и их размер – общие данные для всех объектов.

В заключение этого  отмечу, что в любом классе языка Python мы можем прописывать его описание в виде начальной строки, например, так:

```python
class Point:
    "Класс для представления координат точек на плоскости"
    color = 'red'
    circle = 2
```
В результате, специальная переменная:

```python
Point.__doc__
```

будет ссылаться на это описание. 

Обычно, при создании больших программ, в ключевых классах создают такие описания, чтобы в последующем было удобнее возвращаться к ранее написанному коду, корректировать его и использовать, не обращаясь к специальной документации.


## Резюме

- На самом деле большинство истории ООП в Python сводится до следующего выражения:

```python
объект.атрибут
```


```python
class A:
    x = 5


p1 = A()  # без скобок создается клон
clonA = A  # клон копия класса
p1.x = 10
p1.z = 20
p2 = A()
print(1, p2.x)
A.x = 100
print(p1.__dict__)
print(2, p1.x)  # динамическое изменение (на лету)
print(3, p2.x)  # динамическое изменение (на лету)

print(clonA.__mro__)
print(A.mro())

```

# pastie.org

```python
class A:
    z = 100

class P(A):
    # z = 10

    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y


p1 = P()
print(p1.z)
print(P.mro())

# https://www.geogebra.org/geometry
```


**Классы**

Служат фабриками экземпляров. 

Атрибуты классов обеспечивают поведение (данные и функции), которое наследуется всеми экземплярами, сгенерированными из них (например, функция для расчета заработной платы сотрудника на основе оклада и отработанных часов).



> В чем разница между экземпляром и объектом в `Python`?

В `Python` все является объектом.

* Более того, в Python объекты являются экземплярами классов, из чего следует, что каждый объект также является экземпляром некоторого класса.

* Когда вы создаете экземпляр класса, этот экземпляр является объектом.

* Другими словами, каждый экземпляр является объектом. Это буквальное определение объекта: экземпляр класса.

> Что появилось раньше, яйцо или курица? 


Однако мы обычно используем термин "экземпляр" вместо "объект", когда хотим обсудить поведение экземпляров определенного класса или групп классов
